package com.gjs.common.manager;

import com.gjs.common.pojo.dto.upload.ChunkDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileExistsException;
import org.springframework.util.Assert;

import java.io.*;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 文件分块上传工具类
 *
 * @author JC_Ho 2022年4月7日
 */
@Slf4j
public class FileChunkUtils {

    /**
     * 写入分块
     *
     * @param targetFile      String
     * @param chunkBufferPath String
     * @param chunkDTO        ChunkDTO
     */
    public static boolean upload(String targetFile, String chunkBufferPath, ChunkDTO chunkDTO) throws IOException {
        return upload(targetFile, chunkBufferPath, chunkDTO, false);
    }

    /**
     * 写入分块
     *
     * @param targetFile      String
     * @param chunkBufferPath String
     * @param chunkDTO        ChunkDTO
     * @param isOverride      boolean
     */
    public static boolean upload(String targetFile, String chunkBufferPath, ChunkDTO chunkDTO, boolean isOverride) throws IOException {
        String chunkFolder = chunkBufferPath + chunkDTO.getIdentifier() + File.separator;
        // 目标文件是否已存在
        if (Files.exists(Paths.get(targetFile))) {
            if (isOverride) {
                // 覆盖模式则先删除已存在的目标文件
                Files.delete(Paths.get(targetFile));
            } else {
                // 非覆盖模式而且文件大小不为0就直接返回true
                if (Files.size(Paths.get(targetFile)) > 0) {
                    return true;
                }
            }
        }
        // 写入分块前先判断文件在分块缓冲区中是否已存在
        String checkFile = chunkFolder + File.separator + chunkDTO.getFilename();
        if (Files.exists(Paths.get(checkFile))) {
            // 如果文件已存在，就判断与目标文件路径是否一样，如果一样，就直接复制一份过去就好了
            if (!targetFile.equals(checkFile)) {
                Files.copy(Paths.get(checkFile), Paths.get(targetFile));
                return true;
            }
        } else {
            // 写入分块
            writeChunk(chunkBufferPath, chunkDTO);
            // 判断是否全部分块已上传完毕，如全部上传完，就执行分块合并
            List<Path> chunkPathList = getChunkPathList(chunkFolder, chunkDTO.getFilename());
            if (chunkPathList != null && chunkPathList.size() == chunkDTO.getTotalChunks()) {
                // 合并分块
                mergeChunk(targetFile, chunkPathList);
            }
        }

        return true;
    }

    /**
     * 写入分块
     *
     * @param chunkBufferPath String
     * @param chunkDTO        ChunkDTO
     */
    public static boolean writeChunk(String chunkBufferPath, ChunkDTO chunkDTO) throws IOException {
        // 获取分块路径
        String chunkFilename = generateChunkFilename(chunkBufferPath, chunkDTO);
        // 如果该路径下已存在相同分块，则直接返回true
        if (Files.exists(Paths.get(chunkFilename))) {
            return true;
        }
        // 通过bufferedOutputStream写入分块数据
        try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(chunkFilename))) {
            bufferedOutputStream.write(chunkDTO.getFile().getBytes());
        }

        return true;
    }

    /**
     * 合并分块
     *
     * @param targetFile    String
     * @param chunkPathList List<Path>
     */
    public static boolean mergeChunk(String targetFile, List<Path> chunkPathList) throws IOException {
        Assert.isTrue(!targetFile.endsWith(File.separator), "目标文件路径不能为目录");
        // 检查目标文件是否已存在
        if (Files.exists(Paths.get(targetFile))) {
            if (Files.size(Paths.get(targetFile)) > 0) {
                throw new FileExistsException("目标文件已存在，而且不为空");
            }
        } else {
            if (!Files.isWritable(Paths.get(targetFile).getParent())) {
                Files.createDirectories(Paths.get(targetFile).getParent());
            }
            // 目标文件不存在就创建
            try {
                Files.createFile(Paths.get(targetFile));
            } catch (IOException e) {
                throw new NoSuchFileException("目标目录不存在");
            }
        }

        // Buffered方式，这种方式和下面通过FileChannel方式性能差不多，但这里保留记录一下咯
//        try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(Paths.get(targetFile).toFile()))) {
//            for (Path path : chunkPathList) {
//                try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(path.toFile()))){
//                    // 缓存数组
//                    byte[] buffer = new byte[1024 * 1024];
//                    // 每次读入的字节数量
//                    int inSize = -1;
//                    // 批量读入字节到buffer缓存中,并返回读入的本身数量给inSize
//                    while ((inSize = in.read(buffer)) != -1) {
//                        // 把buffer缓存中的字节写入输出流(也就是目标文件)
//                        out.write(buffer, 0, inSize);
//                    }
//                }
//                //合并后删除该块
//                Files.delete(path);
//            }
//        }

        try (FileChannel resultFileChannel = new FileOutputStream(Paths.get(targetFile).toFile(), true).getChannel()) {
            for (Path path : chunkPathList) {
                //以追加的形式写入分块
                try (FileChannel blk = new FileInputStream(path.toFile()).getChannel()) {
                    resultFileChannel.transferFrom(blk, resultFileChannel.size(), blk.size());
                }
                //合并后删除该块
                Files.delete(path);
            }
        }

        return true;
    }

    /**
     * 按顺序获取指定文件的全部分块
     *
     * @param chunkFolder String
     * @param filename    String
     */
    private static List<Path> getChunkPathList(String chunkFolder, String filename) throws IOException {
        try (Stream<Path> stream = Files.list(Paths.get(chunkFolder))) {
            return stream
                    .filter(path -> path.toString().startsWith(chunkFolder + filename + "-")) // 过滤分块
                    .sorted(Comparator.naturalOrder()) // 分块排序
                    .collect(Collectors.toList());
        }
    }

    /**
     * 获取分块保存路径
     *
     * @param chunkBufferPath String
     * @param chunkDTO        ChunkDTO
     */
    private static String generateChunkFilename(String chunkBufferPath, ChunkDTO chunkDTO) throws IOException {
        Assert.isTrue(chunkBufferPath.endsWith(File.separator), "缓冲目录路径必须以" + File.separator + "结尾");
        String chunkFolder = chunkBufferPath + File.separator + chunkDTO.getIdentifier() + File.separator;
        //判断 chunkFolder/identifier 路径是否存在，不存在就创建
        if (!Files.isWritable(Paths.get(chunkFolder))) {
            try {
                Files.createDirectories(Paths.get(chunkFolder));
            } catch (Exception e) {
                throw new IOException("创建分块缓冲目录失败！");
            }
        }

        return chunkFolder + chunkDTO.getFilename() + "-" + chunkDTO.getChunkNumber();
    }
}